#region File Description
//-----------------------------------------------------------------------------
// AnimationPlayer.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion

#region Using Statements
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
#endregion

namespace XNADota.AnimationModelXFile
{
    /// <summary>
    /// The animation player is in charge of decoding bone position
    /// matrices from an animation clip.
    /// </summary>
    public class AnimationPlayer
    {
        #region Fields


        // Information about the currently playing animation clip.
        private float animationSpeed = 1.0f;
        AnimationClip currentClipValue;
        TimeSpan currentTimeValue;
        bool loop;

        // Current animation transform matrices.
        Matrix[] boneTransforms;
        Matrix[] worldTransforms;
        Matrix[] skinTransforms;

        // records bones and corresponding key frame, used for interpolation
        IList<Keyframe>[] keyFramesEachBone;
        int[] currentKeyframe;

        // Back link to the bind pose and skeleton hierarchy data.
        SkinningData skinningDataValue;

        #endregion


        /// <summary>
        /// Constructs a new animation player.
        /// </summary>
        public AnimationPlayer(SkinningData skinningData)
        {
            if (skinningData == null)
                throw new ArgumentNullException("skinningData");

            skinningDataValue = skinningData;

            boneTransforms = new Matrix[skinningData.BindPose.Count];
            worldTransforms = new Matrix[skinningData.BindPose.Count];
            skinTransforms = new Matrix[skinningData.BindPose.Count];
        }

        public void Reset()
        {
            currentClipValue = null;
            currentTimeValue = TimeSpan.Zero;
            for (int i = 0; i < currentKeyframe.Length; i++) currentKeyframe[i] = 0; ;
        }


        /// <summary>
        /// Starts decoding the specified animation clip.
        /// </summary>
        public void StartClip(AnimationClip clip, bool loop)
        {
            if (clip == null)
                throw new ArgumentNullException("clip");

            currentClipValue = clip;
            currentTimeValue = TimeSpan.Zero;
            this.loop = loop;

            // Initialize bone transforms to the bind pose.
            skinningDataValue.BindPose.CopyTo(boneTransforms, 0);

            // record
            IList<Keyframe> keyFrames = currentClipValue.Keyframes;

            // Take the count the bones

            keyFramesEachBone = new IList<Keyframe>[skinningDataValue.BindPose.Count];
            currentKeyframe = new int[skinningDataValue.BindPose.Count];
            for (int i = 0; i < keyFramesEachBone.Length; i++)
            {
                keyFramesEachBone[i] = new List<Keyframe>();
                currentKeyframe[i] = 0;
            }

            for (int i = 0; i < keyFrames.Count; i++)
            {
                keyFramesEachBone[keyFrames[i].Bone].Add(keyFrames[i]);
            }
        }


        /// <summary>
        /// Advances the current animation position.
        /// </summary>
        public void Update(TimeSpan time, bool relativeToCurrentTime,
                           Matrix rootTransform)
        {
            UpdateBoneTransforms(time, relativeToCurrentTime);
            UpdateWorldTransforms(rootTransform);
            UpdateSkinTransforms();
        }


        /// <summary>
        /// Helper used by the Update method to refresh the BoneTransforms data.
        /// </summary>
        public void UpdateBoneTransforms(TimeSpan time, bool relativeToCurrentTime)
        {
            if (currentClipValue == null)
                return;

            time = new TimeSpan((long)(time.Ticks * animationSpeed));
            // Update the animation position.
            if (relativeToCurrentTime)
            {
                time += currentTimeValue;

                if (loop)
                {
                    // If we reached the end, loop back to the start.
                    while (time >= currentClipValue.Duration)
                        time -= currentClipValue.Duration;
                }
                else
                {
                    if (time >= currentClipValue.Duration)
                    {
                        Reset();
                        return;
                    }
                }
            }

            if ((time < TimeSpan.Zero) || (time >= currentClipValue.Duration))
                throw new ArgumentOutOfRangeException("time");

            // If the position moved backwards, reset the keyframe index.
            if (time < currentTimeValue)
            {
                for (int i = 0; i < currentKeyframe.Length; i++) currentKeyframe[i] = 0; ;
                skinningDataValue.BindPose.CopyTo(boneTransforms, 0);
            }

            currentTimeValue = time;

            // Read keyframe matrices.
            IList<Keyframe> keyframes = currentClipValue.Keyframes;

            // linear interpolation
            for (int i = 0; i < currentKeyframe.Length; i++)
            {
                for (int j = currentKeyframe[i]; j < keyFramesEachBone[i].Count; j++)
                    if (keyFramesEachBone[i][currentKeyframe[i]].Time < currentTimeValue)
                        currentKeyframe[i]++;

                if (currentKeyframe[i] < keyFramesEachBone[i].Count)
                {
                    if (currentKeyframe[i] > 0)
                    {
                        float amount = (float)(currentTimeValue - keyFramesEachBone[i][currentKeyframe[i] - 1].Time).Ticks /
                                    (float)(keyFramesEachBone[i][currentKeyframe[i]].Time - keyFramesEachBone[i][currentKeyframe[i] - 1].Time).Ticks;
                        boneTransforms[i] =
                            Matrix.Lerp(keyFramesEachBone[i][currentKeyframe[i] - 1].Transform,
                                        keyFramesEachBone[i][currentKeyframe[i]].Transform, amount);
                    }
                    else
                        boneTransforms[i] = keyFramesEachBone[i][currentKeyframe[i]].Transform;
                }
                else
                    boneTransforms[i] = Matrix.Identity;
            }
        }


        /// <summary>
        /// Helper used by the Update method to refresh the WorldTransforms data.
        /// </summary>
        public void UpdateWorldTransforms(Matrix rootTransform)
        {
            // Root bone.
            worldTransforms[0] = boneTransforms[0] * rootTransform;

            // Child bones.
            for (int bone = 1; bone < worldTransforms.Length; bone++)
            {
                int parentBone = skinningDataValue.SkeletonHierarchy[bone];

                worldTransforms[bone] = boneTransforms[bone] *
                                             worldTransforms[parentBone];
            }
        }


        /// <summary>
        /// Helper used by the Update method to refresh the SkinTransforms data.
        /// </summary>
        public void UpdateSkinTransforms()
        {
            for (int bone = 0; bone < skinTransforms.Length; bone++)
            {
                skinTransforms[bone] = skinningDataValue.InverseBindPose[bone] *
                                            worldTransforms[bone];
            }
        }


        /// <summary>
        /// Gets the current bone transform matrices, relative to their parent bones.
        /// </summary>
        public Matrix[] GetBoneTransforms()
        {
            return boneTransforms;
        }


        /// <summary>
        /// Gets the current bone transform matrices, in absolute format.
        /// </summary>
        public Matrix[] GetWorldTransforms()
        {
            return worldTransforms;
        }


        /// <summary>
        /// Gets the current bone transform matrices,
        /// relative to the skinning bind pose.
        /// </summary>
        public Matrix[] GetSkinTransforms()
        {
            return skinTransforms;
        }


        /// <summary>
        /// Gets the clip currently being decoded.
        /// </summary>
        public AnimationClip CurrentClip
        {
            get { return currentClipValue; }
        }


        /// <summary>
        /// Gets the current play position.
        /// </summary>
        public TimeSpan CurrentTime
        {
            get { return currentTimeValue; }
        }
    }
}
